package hello.word.m2.maven.support;

import com.google.common.collect.Iterables;
//import lombok.NonNull;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;

import javax.annotation.Nullable;
import javax.persistence.Column;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Set;
import java.util.stream.Collectors;

import static java.lang.String.format;

public abstract class AbstractEntity<ID> implements Entity<ID> {

	@Override
	public void setField(String fieldOrColumnName, String value) {
		final Field field = getField(fieldOrColumnName);
		final Object toWrite;
		// TODO::add converters
		if (field.getType() == LocalDateTime.class) {
			try {
				toWrite = LocalDateTime.parse(value);
			} catch (Exception ex) {
				throw new DaoException(format("can not parse value[%s] by class(LocalDateTime)", value), ex);
			}
		} else {
			toWrite = "null".equals(value) ? null : value;
		}
		try {

			FieldUtils.writeField(getField(fieldOrColumnName), this, toWrite, true);
		} catch (Exception e) {
			throw new DaoException(format("Can not set value[%s] to field[%s]", value, field), e);
		}
	}

	@Override
	public Object getFieldValue(String fieldOrColumnName) {
		return getValue(getField(fieldOrColumnName));
	}

	@Override
	public String getFieldValueAsString(String fieldOrColumnName) {
		Object fieldValue = getFieldValue(fieldOrColumnName);
		return fieldValue == null ? null : fieldValue.toString();
	}

	@Override
	public boolean hasField(/*@NonNull*/ String fieldOrColumnName) {
		return !get(fieldOrColumnName).isEmpty();
	}

	@Override
	public KeyValueMap allFields() {
		return new KeyValueMap(FieldUtils.getFieldsListWithAnnotation(getClass(), Column.class).stream()
				.collect(HashMap::new, (m, v) -> {
					if (!m.containsKey(getName(v))) {
						m.put(getName(v), getValue(v));
					}
				}, HashMap::putAll));
	}

	// region Private
	private Set<Field> get(/*@NonNull*/ String fieldOrColumnName) {
		Set<Field> res = FieldUtils.getFieldsListWithAnnotation(getClass(), Column.class).parallelStream()
				.filter(field -> {
					final String columnName = field.getAnnotation(Column.class).name();
					final String fieldName = field.getName();
					return StringUtils.equalsAnyIgnoreCase(fieldOrColumnName, columnName, fieldName);
				}).collect(Collectors.toSet());
		Set<Field> declaredRes = res.stream().filter(f -> f.getDeclaringClass() == getClass())
				.collect(Collectors.toSet());
		if (declaredRes.isEmpty()) {
			return res;
		} else {
			return declaredRes;
		}
	}

	@Nullable
	private Object getValue(/*@NonNull*/ Field field) {
		field.setAccessible(true);
		try {
			return field.get(this);
		} catch (IllegalAccessException e) {
			throw new DaoException(e);
		}
	}

	/*@NonNull*/
	private String getName(/*@NonNull*/ Field field) {
		final Column column = field.getAnnotation(Column.class);
		if (column != null && !StringUtils.isBlank(column.name())) {
			return column.name();
		}
		return field.getName();
	}

	/*@NonNull*/
	public Field getField(String fieldOrColumnName) {
		Set<Field> values = get(fieldOrColumnName);
		if (values.isEmpty()) {
			throw new DaoException(format("can not found the field[%s] within class[%s].", fieldOrColumnName,
					getClass().getSimpleName()));
		}
		if (values.size() > 1) {
			throw new DaoException(
					format("duplicate field[%s] within class[%s].", fieldOrColumnName, getClass().getSimpleName()));
		}
		return Iterables.getLast(values);
	}
	// endregion
}